Skip to content

feat: TEAV JDBC insert/update [DHIS2-21378]#24098

Merged
enricocolasante merged 12 commits into
masterfrom
DHIS2-21378-teav
Jun 11, 2026
Merged

feat: TEAV JDBC insert/update [DHIS2-21378]#24098
enricocolasante merged 12 commits into
masterfrom
DHIS2-21378-teav

Conversation

@enricocolasante

@enricocolasante enricocolasante commented Jun 2, 2026

Copy link
Copy Markdown
Contributor

Summary

Final slice of the tracker importer Hibernate→JDBC write-path migration DHIS2-21378: moves
TrackedEntityAttributeValue (TEAV) INSERT/UPDATE/DELETE from the EntityManager to JDBC
batching in EntityWriteBatch, and removes the EntityManager from the persister layer
entirely. With this PR, every write in the tracker import commit phase goes through JDBC on the
transaction-bound connection — no Hibernate-managed entities are written during commit, so the
L1 refresh/detach reconciliation that earlier phases needed is deleted too.

Changes

Write path (dhis-tracker)

  • EntityWriteBatch: TEAVs are now flushed via JDBC against trackedentityattributevalue — a
    multi-row INSERT, a single unnest UPDATE keyed on the composite (trackedentityid,
    trackedentityattributeid) PK (created excluded, insert-only), and a single unnest DELETE
    on the same key. No id pre-allocation: the table has a composite PK and no sequence. Values are
    written as plain text (confidential attributes and encryptedvalue were removed in DHIS2-21518).
  • EntityWriteBatch.flush(Connection) no longer takes an EntityManager; the
    refreshUpdatedTrackerEvents/refreshUpdatedSingleEvents L1 reconciliation is removed — there
    are no managed instances left to reconcile.
  • AbstractTrackerPersister: the @PersistenceContext EntityManager field, the JPQL "existing
    TEAVs" lookup, and the per-flush entityManager.flush() calls are gone. Existing attribute
    values are read with a direct JDBC query (loadExistingAttributeValues) on the same
    transaction-bound connection the writes use, producing plain never-managed
    TrackedEntityAttributeValue instances — so a TEAV written by a prior persister in the same
    import (e.g. the same attribute on a TrackedEntity and on an Enrollment payload) is found and
    routed to an UPDATE instead of a duplicate INSERT.
  • idScheme correctness: the lookup resolves attributes from the preheat by primary key, not by
    UID — the preheat maps are keyed by the import's configured idScheme, so a UID-based resolution
    would silently miss every existing value under idScheme=CODE/NAME/ATTRIBUTE and turn updates
    into composite-PK violations.
  • The transaction-bound Connection is threaded through updateAttributes (all five persisters)
    instead of re-acquiring a connection per tracked entity.
  • persistOwnership javadoc documents the one remaining Hibernate write (TrackedEntityProgramOwner on enrollment) and its failure-attribution trade-off: a constraint
    violation (concurrent-import race) surfaces at commit, outside the per-entity try/catch.
  • ⚠️ Audit note: TEAV is @Auditable(scope=TRACKER), and JDBC writes bypass Hibernate's audit
    listeners — consistent with the other migrated entity types. TRACKER-scope auditing is disabled
    by default, and attribute-value history is covered by the trackedentitychangelog table.

Tests

  • The three changelog controller tests gain a startNewRequestSession() helper (flush + clear
    between imports): production gives each /tracker request a fresh transaction-scoped session,
    while MockMvc tests share one thread-bound session — since the importer writes via JDBC, a
    stale managed entity would otherwise leak into the next import's preheat.
  • Several integration tests clear the session after imports for the same reason
    (manager.clear() / dbmsManager.clearSession()).
  • TrackerEventSMSTest: uses a second-precision occurred date — SMS encoding drops milliseconds,
    which the previous Hibernate write path masked.

Performance

Compared against master-without-DHIS2-21378 (GH Actions "Performance tests compare" runs
27197625605 alloc,
27202162811 cpu,
27202229361 wall, 2026-06-09).
Note the baseline excludes the whole migration, so these numbers measure the cumulative
DHIS2-21378 win with this branch as the final state — not this PR's delta alone.

Dimension Metric Baseline This branch Δ
Allocation (-e alloc) total bytes allocated 10.67 GiB 7.27 GiB −32%
CPU (-e cpu) total samples 5506 3748 −32%
Wall (-e wall) import-path samples 2187 875 −60%

Drivers: Hibernate flush/dirty-check/snapshot machinery eliminated (alloc −2.9 GiB, CPU −2127
samples), per-entity statements collapsed into multi-row INSERTs / unnest UPDATEs (DB socket
wait 1554 → 559 samples; jsonb now serialized once instead of deep-copied + compared per flush).
Phase-level breakdown shows the commit phase is now ~90% irreducible DB write time — remaining
opportunities are all read/validation-path and belong in separate tickets.

Verdict: large improvement; the write path is at its structural floor.

Next Steps

Done

To do

  • ⬜ Dedicated Flyway sequences for relationship/relationshipitem (both currently draw from the
    shared hibernate_sequence; RelationshipItem.id is an int, so the global counter risks
    silent narrowing)
  • ⬜ Share EntityWriteBatch across persisters, making the batch the source of truth for staged TEAVs and dropping the per-TE existing-values round-trip (TODO in handleTrackedEntityAttributeValues)
  • ⬜ Reorganize code around persisters and EntityWriteBatch

@sonarqubecloud

sonarqubecloud Bot commented Jun 10, 2026

Copy link
Copy Markdown

@enricocolasante
enricocolasante marked this pull request as ready for review June 10, 2026 16:30
@enricocolasante
enricocolasante requested a review from a team as a code owner June 10, 2026 16:30
@enricocolasante
enricocolasante merged commit 4e3367c into master Jun 11, 2026
23 checks passed
@enricocolasante
enricocolasante deleted the DHIS2-21378-teav branch June 11, 2026 08:48
enricocolasante added a commit that referenced this pull request Jul 7, 2026
… (2.43) (#24286)

* feat: remove EntityManager from TrackerPersister [DHIS2-21378] (#23797)

* feat: remove EntityManager from TrackerPersister interface and method parameters [DHIS2-21378]

* Use default timezone

(cherry picked from commit 052d3ac)

* feat: decouple ChangeLogAccumulator from Hibernate Session [DHIS2-21378] (#23836)

(cherry picked from commit 0c1779f)

* chore: Extract FileResource assignment update [DHIS2-21378] (#23879)

* chore: Extract FileResource assignment update [DHIS2-21378]

* Code review fixes

* Code review fixes

(cherry picked from commit 83542f4)

* feat: stage TEAV writes through EntityWriteBatch [DHIS2-21378] (#23970)

(cherry picked from commit b30c1ce)

* feat: Batch entity when persisting [DHIS2-21378] (#24005)

(cherry picked from commit 100dff1)

* feat: remove EntityManager from DefaultTrackerBundleService [DHIS2-21378] (#24024)

(cherry picked from commit 3759453)

* feat: TrackedEntity JDBC insert [DHIS2-21378] (#24027)

(cherry picked from commit 4eaf6ba)

* feat: TrackedEntity JDBC update [DHIS2-21378] (#24079)

(cherry picked from commit d4af287)

* feat: Enrollment JDBC insert/update [DHIS2-21378] (#24081)

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* Remove L2 caching from enrollment

* Code review fixes

* Code review fixes

(cherry picked from commit 34d629e)

* feat: Tracker event JDBC insert/update [DHIS2-21378] (#24082)

* feat: TrackedEntity JDBC insert [DHIS2-21378]

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

(cherry picked from commit 435872b)

* feat: Single event JDBC insert/update [DHIS2-21378] (#24083)

* feat: TrackedEntity JDBC insert [DHIS2-21378]

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* Format code

(cherry picked from commit 9703e4f)

* feat: Relationship JDBC insert/update [DHIS2-21378] (#24093)

* feat: TrackedEntity JDBC insert [DHIS2-21378]

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* feat: Relationship JDBC insert/update [DHIS2-21378]

(cherry picked from commit 658f0a8)

* feat: TEAV JDBC insert/update [DHIS2-21378] (#24098)

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* feat: Relationship JDBC insert/update [DHIS2-21378]

* feat: TEAV JDBC insert/update [DHIS2-21378]

* Code review fixes

* Fix tests

* Fix idScheme issue for TEAV

(cherry picked from commit 4e3367c)

* fix: Refactor persistence layer in tracker importer [DHIS2-21378] (#24197)

* feat: TrackedEntity JDBC update [DHIS2-21378]

* feat: Enrollment JDBC insert/update [DHIS2-21378]

* feat: Tracker event JDBC insert/update [DHIS2-21378]

* feat: Single event JDBC insert/update [DHIS2-21378]

* feat: Relationship JDBC insert/update [DHIS2-21378]

* feat: TEAV JDBC insert/update [DHIS2-21378]

* Code review fixes

* Fix tests

* Fix idScheme issue for TEAV

* fix: Refactor persistence layer in tracker importer [DHIS2-21378]

* Fix merge

(cherry picked from commit 194dcd1)

* fix: Harmonize db sequences for tracker [DHIS2-21378] (#24208)

* fix: Harmonize db sequences for tracker [DHIS2-21378]

* Fix relationshipitemid

(cherry picked from commit f52eefa)

* fix: Rename conflicting migration [DHIS2-21378] (#24218)

(cherry picked from commit 7aefc1c)

No-op on 2.43: this PR renamed V2_44_12 -> V2_44_13 to avoid a master-only
Flyway version clash. On 2.43 the migration is numbered V2_43_63 (its own
series), so no rename is needed. Empty commit kept to preserve the full
DHIS2-21378 backport history.

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378] (#24227)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

(cherry picked from commit 331503a)

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378] (#24239)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

(cherry picked from commit 7ba2431)

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378] (#24244)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378

* Code review fixes

* Code review fixes

(cherry picked from commit 2972a82)

* perf: Constant-text unnest INSERTs in tracker import [DHIS2-21378] (#24250)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378

* perf: Constant-text unnest INSERTs in tracker import [DHIS2-21378]

* perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378]

* Revert "perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378]"

This reverts commit 7084379.

* Add tests to cover fields import

(cherry picked from commit 7305f9f)

* perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378] (#24252)

* refactor: Split EntityWriteBatch into per-entity writers [DHIS2-21378]

* chore: Migrate ownership from Hibernate to JDBC [DHIS2-21378]

* Code review fixes

* perf: Batch existing-TEAV lookup in tracker import [DHIS2-21378

* perf: Constant-text unnest INSERTs in tracker import [DHIS2-21378]

* perf: Cache UserInfoSnapshot JSON per batch in tracker import [DHIS2-21378]

* Fix duration based Tracker perf tests

* Fix formatting

(cherry picked from commit a03204c)

* fix: Preserve storedby on tracker JDBC writers for 2.43 [DHIS2-21378]

Backport adaptation. On master, DHIS2-21537 removed storedBy from the
TrackedEntity/Enrollment/Event write path (and renamed TEAV storedBy to
updatedBy). 2.43 deliberately keeps storedBy, so the JDBC writers backported
from master must persist the storedby column that 2.43's tables still have.

TrackerObjectsMapper still sets storedBy on the new-entity branch for TE,
enrollment and events, so storedby is insert-only here (mirrors createdby):
added the storedby column + bind to the INSERT of TrackedEntityWriter,
EnrollmentWriter, SingleEventWriter and TrackerEventWriter. TeavWriter was
already adapted (storedby column + getStoredBy) when the earlier commits were
cherry-picked. Keeps the 2.43-only shouldSetStoredByToAuthenticatedUser* tests
green.

* Fix note persister

* Fix formatting
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants